iT邦幫忙

2025 iThome 鐵人賽

DAY 2
1
Software Development

Python pytest TDD 實戰:從零開始的測試驅動開發系列 第 2

Day 02 - 認識斷言(Assertions) 🚀

  • 分享至 

  • xImage
  •  

今天要做什麼?

昨天我們成功建立了測試環境並寫下第一個測試,今天要深入了解測試的核心 —「斷言(Assertions)」。

想像一下,你正在開發一個使用者註冊功能。產品經理說:「我們需要驗證使用者輸入的 email 格式、密碼強度、年齡範圍...」你心想:「這麼多驗證規則,怎麼確保每一個都正確運作?」

答案就是斷言!斷言是測試的核心,它告訴我們「期望」和「實際」結果是否相符。今天我們要學會使用各種斷言方法,讓測試更精準、更具表達力。

學習目標

今天結束後,你將學會:

  • 理解斷言的作用和重要性
  • 掌握 pytest 的各種斷言語法
  • 學會選擇適當的斷言方法
  • 理解斷言失敗時的調試技巧
  • 實踐常見的驗證場景測試

TDD 學習地圖

第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions) ★ 今天在這裡
├── ...
└── (更多精彩內容待續)

什麼是斷言? 🧮

斷言就像品質檢驗員,負責檢查產品是否符合規格。在測試中,斷言:

  • 驗證結果:檢查函數回傳值是否正確
  • 表達期望:清楚說明我們期望的行為
  • 提供回饋:當測試失敗時給出明確的錯誤訊息

基本斷言語法 📝

昨天我們已經使用了最基本的 assert 語句:

建立 tests/day02/test_basic_assertions.py

def test_uses_assert_for_equality():
    result = 2 + 3
    assert result == 5

def test_equality_vs_identity():
    list1 = [1, 2, 3]
    list2 = [1, 2, 3]
    list3 = list1
    
    # == 檢查值相等
    assert list1 == list2
    
    # is 檢查是否為同一個物件(身份相等)
    assert list1 is not list2
    assert list1 is list3

常用斷言方法 🔧

更新 tests/day02/test_basic_assertions.py

def test_equality_assertions():
    # 相等性檢查
    assert 5 == 5
    assert 'hello' == 'hello'
    
    # 不相等檢查
    assert 5 != 10
    assert 'hello' != 'world'

def test_membership_assertions():
    # in 成員檢查
    assert 'h' in 'hello'
    assert 3 in [1, 2, 3, 4, 5]
    
    # not in 檢查
    assert 'x' not in 'hello'
    assert 10 not in [1, 2, 3, 4, 5]

def test_none_assertions():
    value = None
    not_none = 'exists'
    
    assert value is None
    assert not_none is not None

def test_boolean_assertions():
    assert True
    assert not False
    
    # 真值檢查
    assert bool(1)
    assert bool('hello')
    assert not bool(0)
    assert not bool('')

def test_type_assertions():
    assert isinstance(42, int)
    assert isinstance('hello', str)
    assert isinstance(3.14, float)
    assert isinstance([], list)
    assert isinstance({}, dict)

數值比較斷言

數值比較在驗證功能中經常用到:

建立 tests/day02/test_number_assertions.py

def test_greater_than():
    assert 10 > 5
    
def test_greater_than_or_equal():
    assert 10 >= 10
    assert 15 >= 10
    
def test_less_than():
    assert 5 < 10
    
def test_less_than_or_equal():
    assert 5 <= 5
    assert 3 <= 5

def test_range_check():
    value = 5
    assert 1 <= value <= 10
    
def test_approximate_equality():
    import math
    # 使用 pytest.approx 處理浮點數
    import pytest
    assert 0.1 + 0.2 == pytest.approx(0.3)
    assert math.pi == pytest.approx(3.14159, rel=1e-5)

字串相關斷言

字串驗證在表單處理中非常重要:

建立 tests/day02/test_string_assertions.py

def test_string_contains():
    assert 'World' in 'Hello World'
    assert '@' in 'user@example.com'

def test_string_regex():
    import re
    assert re.match(r'^hello\d+$', 'hello123')
    assert re.match(r'\w+@\w+\.\w+', 'test@email.com')

def test_string_length():
    assert len('hello') == 5
    assert len('') == 0

def test_string_starts_ends():
    text = 'Hello World'
    assert text.startswith('Hello')
    assert text.endswith('World')
    
def test_string_case():
    text = 'Hello World'
    assert text.lower() == 'hello world'
    assert text.upper() == 'HELLO WORLD'

實作練習:驗證器函數 🎯

讓我們實作一個驗證器模組來練習各種斷言:

建立 tests/day02/test_validator.py

import pytest
from src.validator import (
    is_valid_email,
    is_strong_password,
    is_valid_age
)

class TestEmailValidation:
    def test_accepts_valid_emails(self):
        assert is_valid_email('user@example.com')
        assert is_valid_email('test.user@company.co.uk')
    
    def test_rejects_invalid_emails(self):
        assert not is_valid_email('invalid')
        assert not is_valid_email('@example.com')
        assert not is_valid_email('user@')

class TestPasswordValidation:
    def test_accepts_strong_passwords(self):
        assert is_strong_password('MyP@ss123')
        assert is_strong_password('Str0ng!Pass')
    
    def test_rejects_weak_passwords(self):
        assert not is_strong_password('weak')
        assert not is_strong_password('onlylowercase')
        assert not is_strong_password('ONLYUPPERCASE')
        assert not is_strong_password('NoNumbers!')

class TestAgeValidation:
    def test_accepts_valid_age_range(self):
        assert is_valid_age(25)
        assert is_valid_age(18)
        assert is_valid_age(65)
    
    def test_rejects_invalid_ages(self):
        assert not is_valid_age(17)
        assert not is_valid_age(121)
        assert not is_valid_age(-5)

實作 Validator 模組

建立 src/validator.py

import re

def is_valid_email(email: str) -> bool:
    email_pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
    return bool(re.match(email_pattern, email))

def is_strong_password(password: str) -> bool:
    if len(password) < 8:
        return False
    
    has_upper = bool(re.search(r'[A-Z]', password))
    has_lower = bool(re.search(r'[a-z]', password))
    has_digit = bool(re.search(r'\d', password))
    has_special = bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password))
    
    return has_upper and has_lower and has_digit and has_special

def is_valid_age(age: int) -> bool:
    return 18 <= age <= 120

執行測試

執行所有 Day 02 的測試:

pytest tests/day02/ -v

你應該會看到所有測試通過,並且能清楚了解每個斷言的作用。

今天學到什麼?

1. 斷言的核心概念

  • 斷言是測試的心臟,負責驗證期望結果
  • 好的斷言讓測試意圖清晰易懂
  • 斷言失敗時提供有價值的調試資訊

2. pytest 斷言方法

  • assert x == y: 相等比較
  • assert x != y: 不相等比較
  • assert x in y: 成員檢查
  • assert x > y, assert x < y: 數值比較
  • assert isinstance(x, type): 類型檢查

3. 斷言選擇策略

  • 選擇最具表達力的斷言方法
  • 使用描述性的變數名稱
  • 複雜邏輯分步驟驗證

4. 實際應用場景

  • 表單驗證邏輯
  • 資料格式化功能
  • 業務規則檢查

總結

今天我們深入學習了斷言的使用,從基本的相等比較到複雜的字串匹配、數值比較。透過實作驗證器函數,我們練習了選擇合適的斷言方法、編寫清晰的測試、分步驟驗證複雜邏輯。

斷言是測試的基石,掌握好斷言的使用,你的測試就會更加準確和有說服力。

明天我們將進入 TDD 的核心 —「紅綠重構循環」! 💪


本文是「Python pytest TDD 實戰:從零開始的測試驅動開發」系列的第二篇文章。我們正在學習測試驅動開發的基礎,從環境設置到掌握核心概念,一步步建立扎實的測試能力。


上一篇
Day 01 - 環境設置與第一個測試 🚀
下一篇
Day 03 - TDD 紅綠重構循環
系列文
Python pytest TDD 實戰:從零開始的測試驅動開發8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言